package IC.Semantics;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;

import IC.BinaryOps;
import IC.AST.ArrayLocation;
import IC.AST.Assignment;
import IC.AST.Break;
import IC.AST.CallStatement;
import IC.AST.Continue;
import IC.AST.Expression;
import IC.AST.ExpressionBlock;
import IC.AST.Field;
import IC.AST.Formal;
import IC.AST.ICClass;
import IC.AST.If;
import IC.AST.Length;
import IC.AST.LibraryMethod;
import IC.AST.Literal;
import IC.AST.LocalVariable;
import IC.AST.LogicalBinaryOp;
import IC.AST.LogicalUnaryOp;
import IC.AST.MathBinaryOp;
import IC.AST.MathUnaryOp;
import IC.AST.Method;
import IC.AST.NewArray;
import IC.AST.NewClass;
import IC.AST.PrimitiveType;
import IC.AST.Program;
import IC.AST.Return;
import IC.AST.Statement;
import IC.AST.StatementsBlock;
import IC.AST.StaticCall;
import IC.AST.StaticMethod;
import IC.AST.This;
import IC.AST.UnaryOp;
import IC.AST.UserType;
import IC.AST.VariableLocation;
import IC.AST.VirtualCall;
import IC.AST.VirtualMethod;
import IC.AST.Visitor;
import IC.AST.While;
import IC.Semantics.symbols.BlockSymbolTable;
import IC.Semantics.symbols.ClassSymbol;
import IC.Semantics.symbols.ClassSymbolTable;
import IC.Semantics.symbols.FieldSymbol;
import IC.Semantics.symbols.GlobalSymbolTable;
import IC.Semantics.symbols.MethodSymbol;
import IC.Semantics.symbols.MethodSymbolTable;
import IC.Semantics.symbols.ParamSymbol;
import IC.Semantics.symbols.ReturnValSymbol;
import IC.Semantics.symbols.SemanticKind;
import IC.Semantics.symbols.SemanticSymbol;
import IC.Semantics.symbols.SymbolTable;
import IC.Semantics.symbols.VariableSymbol;
import IC.Semantics.types.AllMightyType;
import IC.Semantics.types.ArrayType;
import IC.Semantics.types.BooleanType;
import IC.Semantics.types.ClassType;
import IC.Semantics.types.IntType;
import IC.Semantics.types.MethodType;
import IC.Semantics.types.SemanticType;
import IC.Semantics.types.TypeTable;

public class SemanticChecker implements Visitor
{
	// the path to the file containing the program
	private String ICFilePath;
	
	// the global symbol table
	private GlobalSymbolTable globalTable;
	
	// a flag to notify if we encountered a main method
	private boolean hasMainMethod;
	
	// a flag to notify if we found a valid library class
	private boolean hasLibraryClass;
	
	// count the loops we are in so we can know if its legal to use break and continue
	private int loopCounter;
	
	// check if we are in static method so we can know if it is legal to use 'this' keyword
	private boolean inStatic;
	
	//new , check if we are in unary operation MINUS so we will check integer boundaries
	private boolean unaryMinusFlag = false;
	
	// set of symbols we used forward reference to define. check upon completion that no symbols exist
	private LinkedHashMap<SemanticSymbol, SemanticError> unresolvedSymbols;
	
	// maps between allmighty types and the types they are supposed to be sub-types of (revealed later in recursion)
	private HashMap<SemanticType, SemanticType> unresolvedTypes;
	private HashMap<SemanticType, UnresolvedErrorData> unresolvedTypeErrors;
	
	// 
	private HashMap<AllMightyType, SemanticSymbol> unresolvedMethodRet;

	
	/**
	 * constructor - 
	 */
	public SemanticChecker(String ICFilePath)
	{
		this.ICFilePath = ICFilePath;
		this.unresolvedSymbols = new LinkedHashMap<SemanticSymbol, SemanticError>();
		this.unresolvedTypes = new HashMap<SemanticType, SemanticType>();
		this.unresolvedTypeErrors = new HashMap<SemanticType, UnresolvedErrorData>();
		
		this.unresolvedMethodRet = new HashMap<AllMightyType, SemanticSymbol>();

		TypeTable.populatePrimitiveTypes();
	}

	
	// =========================================================================
		
	/**
	 * 
	 */
	@Override
	public GlobalSymbolTable visit(Program program) 
	{
		globalTable = new GlobalSymbolTable(ICFilePath);
		
		try 
		{
			// fill the type table with types for each class and global symbol table with symbol for each class
			for (ICClass c : program.getClasses())
			{
				// add new class type to TypeTable, may throw semantic error if class already defined or super class not defined
				TypeTable.addClassType(c);
				
				// insert class symbol entry to global table
				globalTable.insert(new ClassSymbol(c));
				
				// build class hierarchy
				if (!c.hasSuperClass())
					GlobalSymbolTable.classHier.put(c.getName(), new ClassSymbolTable(globalTable, c.getName()));
				else
				{
					// here we already checked that super is defined
					ClassSymbolTable cst = new ClassSymbolTable(GlobalSymbolTable.classHier.get(c.getSuperClassName()), c.getName());
					GlobalSymbolTable.classHier.put(c.getName(), cst);
					GlobalSymbolTable.classHier.get(c.getSuperClassName()).addTable(c.getName(), cst);
				}
				
				
				// check that we have a 'Library' class
				if (c.getName().equals("Library"))
					hasLibraryClass = true;
			}
			
			// check that we have a 'Library' class
			if (!hasLibraryClass)
				throw new SemanticError("no Library Class found", 0, globalTable.getName());

			// create class symbol table for each class and propagate the checks down the AST
			for (ICClass c : program.getClasses())
			{
				// define the parent symbol table to be the global or the super class table
				SymbolTable parentTable = null;
				
				// check if this class extends other class
				if (c.hasSuperClass())
				{
					// get super class
					parentTable = GlobalSymbolTable.classHier.get(c.getSuperClassName());
					
					// check acyclic class hierarchy
					if (parentTable.getParent() != null && parentTable.getName().equals(c.getName()))
						throw new SemanticError("cyclic class hierarchy not allowed", c.getLine(), c.getSuperClassName() + " and " + c.getName());

				}
				// parent table is global table
				else
					parentTable = globalTable;
				
				c.setEnclosingScope(parentTable);
				
				// create symbol table for the class
				ClassSymbolTable cst = (ClassSymbolTable) c.accept(this);
				if (cst == null)
					return null;
				
				// set pointer from class symbol to his table
				globalTable.lookup(c.getName()).setMatchingTable(cst);
				
				// add the class symbol table to its parent table
				parentTable.addTable(c.getName(), cst);
			}
			
			// when we got here we traverses all methods so we know if there is a main or not
			if (!hasMainMethod)
				throw new SemanticError("no main method was found", 0, globalTable.getName());
			
			// check that all symbols are defined
			if (unresolvedSymbols.size() > 0 )
				throw unresolvedSymbols.get(unresolvedSymbols.keySet().iterator().next());
			
		}
		catch (SemanticError e) 
		{
			System.err.println(e.getMessage());
			return null;
		}
		
		return globalTable;
	}

	/**
	 * 
	 */
	@Override
	public ClassSymbolTable visit(ICClass icClass) 
	{
		ClassSymbolTable cst = GlobalSymbolTable.classHier.get(icClass.getName());
		
		// create field symbols 
		for (Field f : icClass.getFields())
		{
			f.setEnclosingScope(cst);
			
			try
			{
				FieldSymbol fs = (FieldSymbol) f.accept(this);
				if (fs == null)
					return null;
				
				// check if field was previously defined
				SemanticSymbol unres = cst.lookup(f.getName()); 
				if (unres != null)
				{
					if (!unres.isForwardReferenced())
						throw new SemanticError("field already defined", f.getLine(), f.getName());
					else
						unresolvedSymbols.remove(unres);
				}
				
				//insert field symbol entry to global table
				cst.insert(fs);				
			}
			catch (SemanticError e) 
			{
				System.err.println(e.getMessage());
				return null;
			}
		}
		
		// create method symbols and symbol tables
		for (Method m : icClass.getMethods())
		{
			m.setEnclosingScope(cst);
			
			try 
			{
				MethodSymbol ms = new MethodSymbol(m, icClass.getName());
							
				// check if method is main and if so if there is only a unique main method with
				if (isMainMethod(ms))
				{
					if (!hasMainMethod)
						hasMainMethod = true;
					else
						throw new SemanticError("program can't have more than 1 'main' method", m.getLine(), m.getName());
					
					GlobalSymbolTable.mainClassName = icClass.getName();
				}
												
				// check if method already defined as field in same class or super class
				SemanticSymbol unres = cst.lookup(m.getName());
				if (unres != null && unres instanceof FieldSymbol)
					throw new SemanticError(SemanticError.METHOD_ALREADY_DEFINED + " as field", m.getLine(), m.getName());
				
				// check if method already defined in the same class
				if ((unres = cst.localLookup(m.getName())) != null)
				{
					if (!unres.isForwardReferenced())		
						throw new SemanticError(SemanticError.METHOD_ALREADY_DEFINED, m.getLine(), m.getName());
					else
					{
						// check static - virtual error
						if (ms.getType().isStatic() == true && ((MethodType) unres.getType()).isStatic() == false)
							throw new SemanticError("not a virtual method", m.getLine(), m.getName());
						else if (ms.getType().isStatic() == false && ((MethodType) unres.getType()).isStatic() == true)
							throw new SemanticError("not a static method", m.getLine(), m.getName());
						
						
						SemanticType keyType = ((MethodType) unres.getType()).getReturnType();
						SemanticType neededType = unresolvedTypes.get(keyType);
						
						// check if the newly defined symbol return type is subtype of the needed type
						if (neededType == null || ms.getType().getReturnType().subtypeOf(neededType))
							unresolvedSymbols.remove(unres);
						else
							throw new SemanticError(unresolvedTypeErrors.get(keyType).message, unresolvedTypeErrors.get(keyType).line, ms.getType().getReturnType().getName());
						
						// check that the arguments are of the correct type
						//TODO
						ArrayList<SemanticType> methodArgs = ms.getType().getParametersTypes();
						
						// check that number of arguments is valid
						if (((MethodType) unres.getType()).getParametersTypes().size() > methodArgs.size())
							throw new SemanticError("too many arguments delivered to method", m.getLine(), m.getName());
						
						if (((MethodType) unres.getType()).getParametersTypes().size() < methodArgs.size())
							throw new SemanticError("too few arguments delivered to method", m.getLine(), m.getName());
						
						int argNum = 0;
						for (SemanticType arg : ((MethodType) unres.getType()).getParametersTypes())
						{
							// wrong type argument
							if (!arg.subtypeOf(ms.getType().getParametersTypes().get(argNum)))
								throw new SemanticError("wrong argument type for method call, expected " + methodArgs.get(argNum), m.getLine(), arg.getName());
							
							argNum++;
						}
					}
				}
				
				// check if method is defined in super class with different signature
				MethodSymbol superMethod = (MethodSymbol) cst.getParent().lookup(m.getName());
				if (superMethod != null && (!superMethod.getType().equals(ms.getType()) || ms.getType().isStatic() != superMethod.getType().isStatic())){
					throw new SemanticError("method defined in super class with different signature, can't overload", m.getLine(), m.getName());
				}
				
				// insert method symbol to class symbol table
				cst.insert(ms);
				
				// create method symbol table
				MethodSymbolTable mst = (MethodSymbolTable) m.accept(this);
				if (mst == null)
					return null;
				
				// add method table to class symbol table
				cst.addTable(m.getName(), mst);
			}
			catch (SemanticError e) 
			{
				System.err.println(e.getMessage());
				return null;
			}
		}
		
		return cst;
	}


	/**
	 * This method returns MethodSymbolTable object fits to IC.AST.VirtualMethod
	 */
	@Override
	public MethodSymbolTable visit(VirtualMethod method) {
		return createMethodSymbolTable(method);
	}

	/**
	 * This method returns MethodSymbolTable object fits to IC.AST.StaticMethod
	 */
	@Override
	public MethodSymbolTable visit(StaticMethod method) 
	{					
		//flag StaticMethod for 'This' inside one of its Formals/statements
		inStatic = true;
		MethodSymbolTable mst = createMethodSymbolTable(method);
		inStatic = false;
		
		return mst;
	}

	/**
	 * This method returns MethodSymbolTable object fits to IC.AST.LibraryMethod
	 */
	@Override
	public MethodSymbolTable visit(LibraryMethod method) 
	{
		inStatic = true;
		MethodSymbolTable mst = createMethodSymbolTable(method);
		inStatic = false;
		
		return mst;
	}
	

	/**
	 * 
	 * @param field
	 * @return
	 */
	@Override
	public FieldSymbol visit(Field field) 
	{
		FieldSymbol fs = null;

		field.getType().setEnclosingScope(field.getEnclosingScope());
		if (field.getType().accept(this) == null)
			return null;
		
		fs = new FieldSymbol(field);
		
		return fs;
	}

	/**
	 * 
	 * @param formal
	 * @return
	 */
	@Override
	public ParamSymbol visit(Formal formal) 
	{
		ParamSymbol ps = null;

		formal.getType().setEnclosingScope(formal.getEnclosingScope());
		if (formal.getType().accept(this) == null)
			return null;
		
		ps = new ParamSymbol(formal.getName(), formal.getType());
		
		return ps;
	}

	/**
	 * @return the semantic type of the AST primitive
	 */
	@Override
	public SemanticType visit(PrimitiveType type)
	{
		return TypeTable.getType(type);
	}

	/**
	 * return the semantic type of the AST user-defined type  
	 */
	@Override
	public SemanticType visit(UserType type) 
	{	
		SemanticType t = TypeTable.getType(type);
		if (t == null)
			System.err.println(new SemanticError("class not defined", type.getLine(), type.getName()));
		return t;
	}
	

	/**
	 * check that the assignment type is a sub-type of the variable assigning to
	 */
	@Override
	public Object visit(Assignment assignment) 
	{
		assignment.getVariable().setEnclosingScope(assignment.getEnclosingScope());
		SemanticType lhsVar = (SemanticType) assignment.getVariable().accept(this);
		
		// validate lhs var
		if (lhsVar == null) 
			return null;
		
		assignment.getAssignment().setEnclosingScope(assignment.getEnclosingScope());
		SemanticType rhsExp = (SemanticType) assignment.getAssignment().accept(this);
		
		// validate rhs Exp
		if (rhsExp == null)
			return null;
		
		// verify the right hand side expression is a subtype of the left hand side variable type
		try 
		{
			if (rhsExp instanceof AllMightyType)
			{
				unresolvedTypes.put(rhsExp, lhsVar);
				unresolvedTypeErrors.put(rhsExp, new UnresolvedErrorData(assignment.getLine(), "invalid assignment, is not a subtype of " + lhsVar.getName()));
			}
				
			else if (!rhsExp.subtypeOf(lhsVar) && !(lhsVar instanceof AllMightyType))
				throw new SemanticError("invalid assignment, is not a subtype of " + lhsVar.getName(), assignment.getLine(), rhsExp.getName());
		}
		catch (SemanticError e) {
			System.err.println(e.getMessage());
			return null;
		}
		
		return true;
	}
	
	/**
	 * 
	 */
	@Override
	public BlockSymbolTable visit(StatementsBlock statementsBlock) 
	{
		BlockSymbolTable bst = new BlockSymbolTable(statementsBlock.getEnclosingScope());
		BlockSymbolTable parentTable = (BlockSymbolTable) bst.getParent();
		 
		for (Statement s: statementsBlock.getStatements())
		{ 
			s.setEnclosingScope(bst);             
			if (s.accept(this) == null)
				return null;
		}
		
		parentTable.addTable(bst);
		return bst;
	}

	/**
	 * 
	 */
	@Override
	public Object visit(CallStatement callStatement) 
	{
		callStatement.getCall().setEnclosingScope(callStatement.getEnclosingScope());
		return callStatement.getCall().accept(this);
	}

	/**
	 * check that the value returned is a sub-type of of the method return type
	 */
	@Override
	public SemanticType visit(Return returnStatement) 
	{
		// obtain the actual return value
		SemanticType retValType;
		if (returnStatement.hasValue())
		{
			returnStatement.getValue().setEnclosingScope(returnStatement.getEnclosingScope());
			retValType = (SemanticType) returnStatement.getValue().accept(this);
		} 
		else 
			retValType= TypeTable.getPrimitiveType(SemanticType.VOID_TYPE_NAME);
		
		// obtain the return value as defined in method signature
		SemanticType methodRetValType = returnStatement.getEnclosingScope().lookup(SemanticKind.RETURN_VAR.name()).getType();
			
		// validate the types
		if (retValType == null || methodRetValType == null)
			return null;
			
		// check that the actual return type is a sub-type of the signature type 
		try 
		{
			if (retValType instanceof AllMightyType)
			{
				unresolvedTypes.put(retValType, methodRetValType);
				unresolvedTypeErrors.put(retValType, new UnresolvedErrorData(returnStatement.getLine(), "invalid return value, must be a subtype of " + methodRetValType.getName()));
			}
			else if (!retValType.subtypeOf(methodRetValType))
				throw new SemanticError("invalid return value, must be a subtype of " + methodRetValType.getName(), returnStatement.getLine(), retValType.getName());
		} 
		catch (SemanticError e) {
			System.err.println(e.getMessage());
			return null;
		} 
				
		return methodRetValType;
	}

	/**
	 * check that the condition is boolean type
	 */
	@Override
	public Object visit(If ifStatement) 
	{
		BlockSymbolTable scope = (BlockSymbolTable) ifStatement.getEnclosingScope();
		ifStatement.getCondition().setEnclosingScope(scope);
		SemanticType conditionType = (SemanticType) ifStatement.getCondition().accept(this);
		
		// validate the condition
		if (conditionType == null)
			return null;
		
		try
		{
			// check the the condition is boolean
			if (!conditionType.subtypeOf(TypeTable.getPrimitiveType(SemanticType.BOOLEAN_TYPE_NAME)))
				throw new SemanticError("condition must be boolean", ifStatement.getLine(), conditionType.getName());
		} catch (SemanticError e) {
			System.err.println(e.getMessage());
			return null;
		}
		
		Object opVisit = statementOpVisit(ifStatement.getOperation(), scope);
		
		if (ifStatement.hasElse())
			if (statementOpVisit(ifStatement.getElseOperation(), scope) == null)
				return null;
		
		return opVisit;
	}
	
	/**
	 * checks that the condition is of type boolean
	 */
	@Override
	public Object visit(While whileStatement) 
	{
		BlockSymbolTable scope = (BlockSymbolTable) whileStatement.getEnclosingScope();
		whileStatement.getCondition().setEnclosingScope(scope);
		SemanticType conditionType = (SemanticType) whileStatement.getCondition().accept(this);
		
		// validate the condition
		if (conditionType == null)
			return null;
		
		try 
		{
			// check the the condition is boolean
			if (!conditionType.subtypeOf(TypeTable.getPrimitiveType(SemanticType.BOOLEAN_TYPE_NAME)))
				throw new SemanticError("condition must be boolean", whileStatement.getLine(), conditionType.getName());
		} catch (SemanticError e) 
		{
			System.err.println(e.getMessage());
			return null;
		}
		
		loopCounter++;
		Object opVisit = statementOpVisit(whileStatement.getOperation(), scope);
		loopCounter--;
		
		return opVisit;
	}

	/**
	 * check that the 'break' keyword is used only inside a loop
	 */
	@Override
	public Object visit(Break breakStatement) 
	{
		try{
			if (loopCounter == 0)
				throw new SemanticError("cannot use 'break' outside of loop", breakStatement.getLine(), "");
		} catch (SemanticError e) {
			System.err.println(e.getMessage());
			return null;
		}
		
		return true;
	}

	/**
	 * checks that the 'continue' keyword is used only inside a loop
	 */
	@Override
	public Object visit(Continue continueStatement) 
	{
		
		try {
			if (loopCounter == 0)
				throw new SemanticError("cannot use 'continue' outside of loop", continueStatement.getLine(), "");
		} catch (SemanticError e) {
			System.err.println(e.getMessage());
			return null;
		}
		
		return true;
	}

	/**
	 * checks local variable scope rules
	 */
	@Override
	public SemanticType visit(LocalVariable localVariable) 
	{
		SymbolTable scope = localVariable.getEnclosingScope();
		SemanticType type = null;
		
		try 
		{
			// check if symbol already defined in the current scope
			SemanticSymbol unres = scope.localLookup(localVariable.getName());
			if (unres != null)
			{
				if (!unres.isForwardReferenced())
					throw new SemanticError("variable already defined", localVariable.getLine(), localVariable.getName());
				else
					unresolvedSymbols.remove(unres);
			}
			
			// check the type of this variable
			localVariable.getType().setEnclosingScope(scope);
			type = (SemanticType) localVariable.getType().accept(this);
			
			// insert the variable to the symbol table
			VariableSymbol vs = new VariableSymbol(localVariable.getName(), localVariable.getLine(), type);
			scope.insert(vs);
			
			// check the initialization value of the variable
			SemanticType initval = null;
			if (localVariable.hasInitValue()){
				localVariable.getInitValue().setEnclosingScope(scope);
				initval = (SemanticType) localVariable.getInitValue().accept(this);
				if (initval == null)
					return null;
				
				vs.isInit = true;
			}
			
			//check if init_value type is the same or subtype of the local variable type
			if (initval != null)
			{
				if (initval instanceof AllMightyType){
					unresolvedTypes.put(initval, type);
					unresolvedTypeErrors.put(initval, new UnresolvedErrorData(localVariable.getLine(), "invalid variable initialization, initial value is not a subtype of " + type.getName()));
				}
				else if (!initval.subtypeOf(type))
					throw new SemanticError("invalid variable initialization, initial value is not a subtype of " + type.getName(), localVariable.getLine(), initval.getName());
			}
			
		} 
		catch (SemanticError e) 
		{
			System.err.println(e.getMessage());
			return null;
		}
		
		return type;
	}

	/**
	 * returns the semantic type of the location
	 */
	@Override
	public SemanticType visit(VariableLocation location) 
	{
		SemanticSymbol locationSym = null;
		SymbolTable symtab = null;
		
		if (location.isExternal()) 
		{ 
            location.getLocation().setEnclosingScope(location.getEnclosingScope());
            SemanticType classType = (SemanticType) location.getLocation().accept(this);
            
            if (classType == null)
            	return null;
            
            // this is a field of some class instance - get this class's symbol table and lookup the variable name
            symtab = GlobalSymbolTable.classHier.get(classType.getName());
            locationSym = symtab.lookup(location.getName());
            
            // check that the variable representing the location was defined in the given class and if not create a forward reference
    		if (locationSym == null)
    		{
    			// create forward referenced symbol
    			locationSym = new FieldSymbol(location.getName());
    			symtab.insert(locationSym);
    			unresolvedSymbols.put(locationSym, new SemanticError("field not defined", location.getLine(), location.getName()));
    		}
		}
		else
		{
			symtab = location.getEnclosingScope();
			locationSym = symtab.lookup(location.getName());
			
			// reference to a field of the current class in static method
			if (locationSym instanceof FieldSymbol && inStatic)
			{
				try	{	
					throw new SemanticError("cannot reference a field in static method ", location.getLine(), location.getName());
				} 
				catch (SemanticError e) {
					System.err.println(e.getMessage());
					return null;
				}
			}
			
			// check that the variable representing the location was defined
			if (locationSym == null)
			{
				// create forward referenced symbol
				locationSym = new VariableSymbol(location.getName());
				symtab.insert(locationSym);
				unresolvedSymbols.put(locationSym, new SemanticError("variable not defined", location.getLine(), location.getName()));
			}
		}
		
		//TODO - bonus
//			else if (locationSym instanceof VariableSymbol){
//				if (!((VariableSymbol) locationSym).isInit)
//					throw new SemanticError("variable must be initialized before being used", location.getLine(), location.getName());
//			}
		
		return locationSym.getType();
	}

	/**
	 * checks that the array index is integer type
	 * return the semantic type of the array elements 
	 */
	@Override
	public SemanticType visit(ArrayLocation location) 
	{
		location.getIndex().setEnclosingScope(location.getEnclosingScope());
		SemanticType indexType = (SemanticType) location.getIndex().accept(this);
		
		if (indexType == null)
			return null;
		
		try 
		{
			if (!indexType.subtypeOf(TypeTable.getPrimitiveType(SemanticType.INT_TYPE_NAME)))
				throw new SemanticError("array index must be int, invalid value", location.getLine(), indexType.getName());
		
			location.getArray().setEnclosingScope(location.getEnclosingScope());
			SemanticType arrType = (SemanticType) location.getArray().accept(this);
			
			// validate array
			if (arrType == null)
				return null;
			//if arrType
			else if (arrType instanceof ArrayType)
				return ((ArrayType) arrType).getElementType();
			//if arrType is AllMightyType
			else if (arrType instanceof AllMightyType) 
				return new AllMightyType();
			else
				throw new SemanticError("array access for a variable that is not an array type", location.getLine(), ((VariableLocation) location.getArray()).getName());
		} 
		catch (SemanticError e) 
		{
			System.err.println(e.getMessage());
			return null;
		}
	}

	/**
	 * check that the class of the static call is defined, that the method exist in it and is static.
	 * check that the arguments delivered are of the correct type.
	 * return the method return type
	 */
	@Override
	public SemanticType visit(StaticCall call)
	{
		MethodSymbol ms = null;
		try
		{
			// check that the class of the method is defined
			ClassSymbol cs = (ClassSymbol) globalTable.lookup(call.getClassName());
			if (cs == null)
				throw new SemanticError("class not defined", call.getLine(), call.getClassName());
			
			ClassSymbolTable cst = cs.getMatchingTable();
			if (cst == null)
				cst = GlobalSymbolTable.classHier.get(cs.getName());
//				unresolvedSymbols.put(cs, new SemanticError("class not defined", call.getLine(), call.getClassName()));
			
			// check that the method exist in the class and is defined static
			ms = (MethodSymbol) cst.localLookup(call.getName());
			if (ms == null)
			{
				AllMightyType dummy = new AllMightyType();
				ArrayList<SemanticType> params = new ArrayList<SemanticType>();
				
				for (Expression e : call.getArguments())
				{
					e.setEnclosingScope(call.getEnclosingScope());
					SemanticType arg = (SemanticType) e.accept(this);
					
					// validate the arg 
					if (arg == null)
						return null;
					params.add(arg);
				}
				
				ms = new MethodSymbol(call.getName(), params, dummy, true);
				cst.insert(ms);
				
				unresolvedSymbols.put(ms, new SemanticError("method doesn't exist", call.getLine(), call.getName()));
				unresolvedMethodRet.put(dummy, ms);
				
				return ms.getType().getReturnType();
			}
			else
			{
				// means it was called virtually before and now called statically (and the method was yet to be defined so we don't know which call is correct)
				if (!ms.getType().isStatic() && ms.isForwardReferenced())
					throw new SemanticError("invalid method call, resolve virtual and static call for method", call.getLine(), call.getName());
					
				// the method was defined as virtual
				if (!ms.getType().isStatic())
					throw new SemanticError("not a static method", call.getLine(), call.getName());
			}
			
			// check that the arguments are valid
			ArrayList<SemanticType> methodArgs = ms.getType().getParametersTypes();
			
			// check that number of arguments is valid
			if (call.getArguments().size() > methodArgs.size())
				throw new SemanticError("too many arguments delivered to method", call.getLine(), call.getName());
			
			if (call.getArguments().size() < methodArgs.size())
				throw new SemanticError("too few arguments delivered to method", call.getLine(), call.getName());
			
			// check that the arguments are of the correct type
			int argNum = 0;
			for (Expression e : call.getArguments())
			{
				e.setEnclosingScope(call.getEnclosingScope());
				SemanticType arg = (SemanticType) e.accept(this);
				
				// validate the arg 
				if (arg == null)
					return null;
				
				// wrong type argument
				if (!arg.subtypeOf(methodArgs.get(argNum)))
					throw new SemanticError("wrong argument type for method call, expected " + methodArgs.get(argNum), e.getLine(), arg.getName());
				
				argNum++;
			}

		}
		catch(SemanticError e)
		{
			System.err.println(e.getMessage());
			return null;
		}
		
		return ms.getType().getReturnType();
	}

	/**
	 * check that the method is defined in the current class scope
	 * check that the arguments delivered are valid 
	 */
	@Override
	public SemanticType visit(VirtualCall call) 
	{
		MethodSymbol ms = null;
		ClassSymbolTable cst = null;
		try
		{
			// if the call has an external location
			if (call.isExternal())
			{
				call.getLocation().setEnclosingScope(call.getEnclosingScope());
				SemanticType loc = (SemanticType) call.getLocation().accept(this);
				
				// validate the location
				if (loc == null)
					return null;
				
				// check that the location is a class
				
				cst = (ClassSymbolTable) globalTable.lookup(loc.getName()).getMatchingTable();
				if (cst == null)
				{
					cst = GlobalSymbolTable.classHier.get(loc.getName());
//					unresolvedSymbols.put(globalTable.lookup(loc.getName()), new SemanticError("invalid method call, location is not a defined class", call.getLine(), loc.getName()));
				}
				
				ms = (MethodSymbol) cst.lookup(call.getName());
			}
			else
			{
				cst = ((BlockSymbolTable)call.getEnclosingScope()).getClassScope();
				ms = (MethodSymbol) call.getEnclosingScope().lookup(call.getName());
				
				// if the call's location is not external, check that we are not in static scope 
				if (inStatic)
					throw new SemanticError("cannot call a local virtual method from inside a static scope", call.getLine(), call.getName());
			}
	
			
			// check that the method exist and is not called statically
			if (ms == null)
			{
				AllMightyType dummy = new AllMightyType();
				ArrayList<SemanticType> params = new ArrayList<SemanticType>();
				
				for (Expression e : call.getArguments())
				{
					e.setEnclosingScope(call.getEnclosingScope());
					SemanticType arg = (SemanticType) e.accept(this);
					
					// validate the arg 
					if (arg == null)
						return null;
					params.add(arg);
				}
				
				ms = new MethodSymbol(call.getName(), params, dummy, false);
				cst.insert(ms);
				
				unresolvedSymbols.put(ms, new SemanticError("method doesn't exist", call.getLine(), call.getName()));
				unresolvedMethodRet.put(dummy, ms);
				return ms.getType().getReturnType();
			}
			else
				if (ms.getType().isStatic() && call.isExternal() )
					throw new SemanticError("calling a static method virtually", call.getLine(), call.getName());
			
			// check that the arguments are valid
			ArrayList<SemanticType> methodArgs = ms.getType().getParametersTypes();
						
			// check that number of arguments is valid
			if (call.getArguments().size() > methodArgs.size())
				throw new SemanticError("too many arguments delivered to method", call.getLine(), call.getName());
			
			if (call.getArguments().size() < methodArgs.size())
				throw new SemanticError("too few arguments delivered to method", call.getLine(), call.getName());
			
			// check that the arguments are of the correct type
			int argNum = 0;
			for (Expression e : call.getArguments())
			{
				e.setEnclosingScope(call.getEnclosingScope());
				SemanticType arg = (SemanticType) e.accept(this);
				
				// validate the arg 
				if (arg == null)
					return null;
				
				// wrong type argument
				if (!arg.subtypeOf(methodArgs.get(argNum)))
					throw new SemanticError("wrong argument type for method call, expected " + methodArgs.get(argNum), e.getLine(), arg.getName());
				
				argNum++;
			}
		}
		catch(SemanticError e)
		{
			System.err.println(e.getMessage());
			return null;
		}
		
		return ms.getType().getReturnType();
	}

	/**
	 * checks that the keyword 'this' is not used inside a static method
	 * return the type of the class 'this' referring to
	 */
	@Override
	public SemanticType visit(This thisExpression) 
	{
		try
		{
			if (inStatic)
				throw new SemanticError("cannot use 'this' keyword inside a static method", thisExpression.getLine(), "");
		}
		catch(SemanticError e)
		{
			System.err.println(e.getMessage());
			return null;
		}
		
		return thisExpression.getEnclosingScope().lookup(MethodSymbolTable.THIS_VAR_NAME).getType();
	}

	/**
	 * checks that the class type of the new instance is defined
	 * returns the semantic class type
	 */
	@Override
	public ClassType visit(NewClass newClass)
	{
		ClassType ct = null	;
		
		ct = TypeTable.getClassType(newClass.getName());
		if (ct == null)
		//TODO 
		{
			ClassSymbol cs = new ClassSymbol(newClass.getName());
			globalTable.insert(cs);
			
			ClassSymbolTable cst = new ClassSymbolTable(globalTable, newClass.getName());
			globalTable.addTable(cst.getName(), cst);
			
			unresolvedSymbols.put(cs, new SemanticError("class not defined", newClass.getLine(), newClass.getName()));
			ct = new ClassType(newClass.getName());
		}
		
		return ct;
	}

	@Override
	public ArrayType visit(NewArray newArray) 
	{
		ArrayType arr = null;
		
		try 
		{
			newArray.getSize().setEnclosingScope(newArray.getEnclosingScope());
			SemanticType size = (SemanticType) newArray.getSize().accept(this);
			
			// validate size
			if (size == null)
				return null;
			if (!size.subtypeOf(TypeTable.getPrimitiveType(SemanticType.INT_TYPE_NAME)))
				throw new SemanticError("array size must be of int type", newArray.getLine(), size.getName());
			
			newArray.getType().setEnclosingScope(newArray.getEnclosingScope());
			SemanticType arrType = (SemanticType) newArray.getType().accept(this);
			
			// validate the array type
			if (arrType == null)
				return null;
			
			// get the array type after it was created by the arrType visitor
			arr = (ArrayType) TypeTable.getArrayType(arrType);
		}
		catch (SemanticError e) {
			System.err.println(e.getMessage());
			return null;
		}
		return arr;
	}

	/**
	 * check that the array of the length object is valid.
	 * return the semantic type int (which describes the length)
	 */
	@Override
	public SemanticType visit(Length length) 
	{
		try
		{
			length.getArray().setEnclosingScope(length.getEnclosingScope());
			SemanticType arrType = (SemanticType) length.getArray().accept(this);
			
			// validate the array 
			if (arrType == null)
				return null;
			
			// note that we use 'instanceof' and not 'subTypeOf' otherwise we would get always false by its implementation in arrayType 
			if (!(arrType instanceof ArrayType))
				throw new SemanticError("not an array type", length.getLine(), arrType.getName());
		}
		catch(SemanticError e)
		{
			System.err.println(e.getMessage());
			return null;
		}
		
		return TypeTable.getPrimitiveType(SemanticType.INT_TYPE_NAME);
	}

	/**
	 * checks that both the operands types are int or string (if op is +)
	 */
	@Override
	public Object visit(MathBinaryOp binaryOp) 
	{
		binaryOp.getFirstOperand().setEnclosingScope(binaryOp.getEnclosingScope());
		SemanticType op1 = (SemanticType) binaryOp.getFirstOperand().accept(this);
		if (op1 == null)
			return null;
		
		binaryOp.getSecondOperand().setEnclosingScope(binaryOp.getEnclosingScope());
		SemanticType op2 = (SemanticType) binaryOp.getSecondOperand().accept(this);
		if (op2 == null)
			return null;
		
		try
		{
			// check if same operands
			if (!op1.subtypeOf(op2) && !op2.subtypeOf(op1))
				throw new SemanticError("can't perform operation on different types", binaryOp.getLine(), binaryOp.getOperator().getOperatorString());
			
			// if operator is anything but '+'
			if (binaryOp.getOperator() != BinaryOps.PLUS)
			{
				if (!op1.subtypeOf(TypeTable.getPrimitiveType(SemanticType.INT_TYPE_NAME)))
					throw new SemanticError("cant preform math operation on non integer type", binaryOp.getLine(), op1.getName());
			}
			else
			{
				// check if op1&op2 are not legal types for PLUS (string/int)
				if  (! (op1.subtypeOf(TypeTable.getPrimitiveType(SemanticType.INT_TYPE_NAME)) || op1.subtypeOf(TypeTable.getPrimitiveType(SemanticType.STRING_TYPE_NAME)))) 
					throw new SemanticError("illegal type for operation '+', must be int or string", binaryOp.getLine(), op1.getName());
			}
			
		}
		catch(SemanticError e)
		{
			System.err.println(e.getMessage());
			return null;
		}
				
		return op1;
	}

	/**
	 * checks that both the operands types are boolean for logical ops (&&, ||), int for comparision ops(<= etc...) or subtypes of each other for equality ops (==, !=)
	 */
	@Override
	public BooleanType visit(LogicalBinaryOp binaryOp) 
	{
		binaryOp.getFirstOperand().setEnclosingScope(binaryOp.getEnclosingScope());
		SemanticType op1 = (SemanticType) binaryOp.getFirstOperand().accept(this);
		if (op1 == null)
			return null;
		
		binaryOp.getSecondOperand().setEnclosingScope(binaryOp.getEnclosingScope());
		SemanticType op2 = (SemanticType) binaryOp.getSecondOperand().accept(this);
		if (op2 == null)
			return null;
		
		try
		{
			BinaryOps operator = binaryOp.getOperator();
			// operator is '&&' or '||'
			if (operator == BinaryOps.LAND || operator == BinaryOps.LOR)
			{
				// check that both operators are boolean
				if (!op1.subtypeOf(TypeTable.getPrimitiveType(SemanticType.BOOLEAN_TYPE_NAME)) || !op2.subtypeOf(TypeTable.getPrimitiveType(SemanticType.BOOLEAN_TYPE_NAME)))
					throw new SemanticError("cant perform operation " + operator.getOperatorString() + " on non boolean types", binaryOp.getLine(), op1.getName() + " and " + op2.getName() );
			}
			// operator is '==' or '!='
			else if(operator == BinaryOps.EQUAL || operator == BinaryOps.NEQUAL)
			{
				// neither of them is a subtype of the other
				if (!op1.subtypeOf(op2) && !op2.subtypeOf(op1))
					throw new SemanticError("cannot compare between different types", binaryOp.getLine(), op1.getName() + " and " + op2.getName());
			}
			// operator is '<', '<=', '>', '>='
			else
			{
				if (!op1.subtypeOf(TypeTable.getPrimitiveType(SemanticType.INT_TYPE_NAME)) || !op2.subtypeOf(TypeTable.getPrimitiveType(SemanticType.INT_TYPE_NAME)))
					throw new SemanticError("cannot compare non integer types", binaryOp.getLine(), op1.getName() + " and " + op2.getName());
			}
			
		}
		catch(SemanticError e)
		{
			System.err.println(e.getMessage());
			return null;
		}
		
		return (BooleanType) TypeTable.getPrimitiveType(SemanticType.BOOLEAN_TYPE_NAME);
	}

	/**
	 * checks that the operand type is int
	 */
	@Override
	public IntType visit(MathUnaryOp unaryOp) 
	{
		SemanticType op = OpVisit(unaryOp);
		
		// validate op
		if (op == null)
			return null;
		
		try {
			// check the operation is int
			if (!op.subtypeOf(TypeTable.getPrimitiveType(SemanticType.INT_TYPE_NAME)))
				throw new SemanticError("cant preform math operation on non integer type", unaryOp.getLine(), op.getName());
		} catch (SemanticError e) {
			System.err.println(e.getMessage());
			return null;
		}
		
		return (IntType) op;
	}

	/**
	 * checks that the operand type is boolean
	 */
	@Override
	public BooleanType visit(LogicalUnaryOp unaryOp) 
	{
		SemanticType op =  OpVisit(unaryOp);

		// validate op
		if (op == null)
			return null;
		
		try {
			// check the operation is boolean
			if (!op.subtypeOf(TypeTable.getPrimitiveType(SemanticType.BOOLEAN_TYPE_NAME)))
				throw new SemanticError("cant preform logical operation on non boolean type", unaryOp.getLine(), op.getName());
		} catch (SemanticError e) {
			System.err.println(e.getMessage());
			return null;
		}
			
		return (BooleanType) op;
	}

	/**
	 * return the semantic type corresponding to the given literal type
	 */
	@Override
	public SemanticType visit(Literal literal) 
	{
		
		try{
			switch (literal.getType())
			{  
				case STRING: return TypeTable.getPrimitiveType(SemanticType.STRING_TYPE_NAME);             
				case INTEGER:
					// new
					if (unaryMinusFlag) { 
						if (Long.parseLong((String)literal.getValue()) > (long) Math.pow(2,31))
							throw new SemanticError("Integer value is out of bound", literal.getLine(), "-"+(String)literal.getValue());
					}
					else {	
						if (Long.parseLong((String)literal.getValue())> (long)Integer.MAX_VALUE)
							throw new SemanticError("Integer value is out of bound", literal.getLine(), (String)literal.getValue());
					}
					
					//literal.setValue(new Integer((String)literal.getValue()));  
					return TypeTable.getPrimitiveType(SemanticType.INT_TYPE_NAME);
				case TRUE: return TypeTable.getPrimitiveType(SemanticType.BOOLEAN_TYPE_NAME);     
				case FALSE: return TypeTable.getPrimitiveType(SemanticType.BOOLEAN_TYPE_NAME);          
				case NULL: return TypeTable.getPrimitiveType(SemanticType.NULL_TYPE_NAME);
			} 
		}catch (SemanticError e) {
			System.err.println(e.getMessage());
			
		}
		return null;
	}

	@Override
	public Object visit(ExpressionBlock expressionBlock) 
	{
		expressionBlock.getExpression().setEnclosingScope(expressionBlock.getEnclosingScope());
		return expressionBlock.getExpression().accept(this);
	}
	
	// =================================================================
	
	
	/**
	 * This method checks if ms is the main method of its class 
	 * @param ms is the given method
	 * @return true if ms has the correct signature of main method 
	 * @throws SemanticError
	 */
	private static boolean isMainMethod(MethodSymbol ms) throws SemanticError
	{
		MethodType mt = ms.getType();
		
		// check if name is main
		if (!ms.getName().equals("main"))
			return false;
		// check if static
		if (!ms.getType().isStatic())
			return false;
		// check return type
		if (!(mt.getReturnType().subtypeOf(TypeTable.getPrimitiveType(SemanticType.VOID_TYPE_NAME)))) 
			return false;

		ArrayList<SemanticType> params = mt.getParametersTypes();
		// there is wrong parameter signature
		if (params == null || params.isEmpty() || params.size() != 1)
			return false;
		// parameter is not of type string[]
		if (!params.get(0).subtypeOf(TypeTable.getArrayType(TypeTable.getPrimitiveType(SemanticType.STRING_TYPE_NAME))))           
			return false;            
		
		return true;
	}
	
	/**
	 * This method creates MethodSymbolTable,  
	 * and continue building Semantic & type tables recursively
	 * @param m is the IC.AST.Method to build for
	 * @return MethodSymbolTable that fits to 1 of 3 method types: VirtualMethod / StaticMethod / LibraryMethod
	 */
	private MethodSymbolTable createMethodSymbolTable(Method m)
	{
		MethodSymbolTable mst = new MethodSymbolTable(m.getEnclosingScope(), m.getName());
		
		try 
		{
			// add 'this' variable to method symbol table if virtual method
			if (!inStatic)
				mst.insert(MethodSymbolTable.THIS_VAR_NAME, new VariableSymbol(m.getEnclosingScope().getName(), 0));
			
			m.getType().setEnclosingScope(mst);

			// check method return type
			SemanticType t = (SemanticType) m.getType().accept(this);
			if (t == null)
				return null;
			
			mst.insert(SemanticKind.RETURN_VAR.name(), new ReturnValSymbol(m.getType()));
			
			// check method parameters
			for (Formal f : m.getFormals())
			{
				f.setEnclosingScope(mst);
				ParamSymbol param = (ParamSymbol) f.accept(this);
				if (param == null)
					return null;
				
				// check if parameter already exist in this method scope -
				// if yes throw semantic error if no - enter him to the table
				SemanticSymbol unres = mst.localLookup(param.getName());
				if (unres != null)
				{
					if (!param.isForwardReferenced())
						throw new SemanticError("parameter already defined", f.getLine(), f.getName());
					else
						unresolvedSymbols.remove(unres);
				}
				
				mst.insert(param);
			}
			
			for (Statement s: m.getStatements())
			{
				s.setEnclosingScope(mst);
				if (s.accept(this) == null)
					return null;
			}
			
		} 
		catch (SemanticError e) 
		{
			System.err.println(e.getMessage());
			return null;
		}			
		
		return mst;
	}
	
	/**
	 * visit the statement and create a new block symbol table if necessary
	 * @param op - the statement we want to visit
	 * @param scope - the current known scope of the visit
	 * @return the result of the accept method of the given statement
	 */
	private Object statementOpVisit(Statement op, BlockSymbolTable scope)
	{
//		if (op instanceof LocalVariable)
//		{
//			BlockSymbolTable bst = new BlockSymbolTable(scope);
//			scope.addTable(bst);
//			op.setEnclosingScope(bst);
//		}
//		else
			op.setEnclosingScope(scope);
		
		return op.accept(this);
	}
	
	private SemanticType OpVisit(UnaryOp op)
	{
		//new
		unaryMinusFlag = true;
		op.getOperand().setEnclosingScope(op.getEnclosingScope());
		SemanticType st =  (SemanticType) op.getOperand().accept(this);
		unaryMinusFlag = false;
		return st;
	}
	
}
